Skip to content

Split the project upload and update handlers into named steps - #72

Merged
Mistium merged 8 commits into
mainfrom
split-project-handlers
Nov 1, 2026
Merged

Mistium merged 8 commits into
mainfrom
split-project-handlers

Conversation

@Mistium

@Mistium Mistium commented Nov 1, 2026

Copy link
Copy Markdown
Member

The two largest request handlers each did everything inline. handleUploadProjectLocked (565 lines) and handleUpdateProjectLocked (485) are now short lists of named steps, 63 and 45 lines. No behaviour changes.

src/projects/
  project-refusal.osl         Refusal record and constructors, shared by both handlers
  project-request.osl         loadRequestedProject, refused(c, r), isEditorSession
  project-upload.osl          route handlers; handleUploadProjectLocked as a list of steps
  project-upload-policy.osl   UploadRequest / UploadHistory / HistoryIdentity records and pure decisions
  project-upload-receive.osl  form, files and staged parts; unpacking the sb3
  project-upload-history.osl  workspace checks, no-change check, merge and restore commits, storing history
  project-upload-files.osl    uploadProjectFiles and its failure helpers
  project-upload-publish.osl  rollback state, extension store, record update, JSON publish, cleanup, response
  project-update.osl          handleUpdateProjectLocked as steps
  project-update-policy.osl   pure field rules and the OwnerTransfer record
  project-team.osl            collaborator validation and new-teammate notifications

Each step that can refuse returns a Refusal, so every early exit keeps its status, body and order. State between steps goes through small records rather than shared locals.

Smoke test: scripts/e2e_smoke.py grew from 28 to over 200 steps.

  • Covered: plain, asset, sparse, workspace, delta, restore, branch-merge, pull-merge, history-replace and live-editor uploads, plus every upload failure reachable offline (400, 404, 409 and 413). Also every update field and its refusals, team changes, ownership transfer, and a non-ASCII project that tells bytes from characters.
  • Compared exactly: responses, stored project records after each step, and asset and project-json blob names. Only genuinely volatile fields are masked.

Verification

  • Route dump: identical, 1,158 lines.
  • Smoke diff: the base binary (main) against this branch, both running the new smoke script, gives "responses match".
  • Checks: osl test (51 pass), osl lsp check and check_conventions pass.
  • Review: an independent review walked all of the roughly 85 early returns and confirmed each one refuses at the same point, with the same status, body and cleanup order. Its findings are fixed here: the smoke test masked too much, the refusal fallback could send 500 for other statuses, and helpers sat in the wrong files.

The same work found pre-existing bugs in these paths, including locks that stay held after a thrown error. They are fixed separately in a stacked follow-up.

There is no visible change.

Exercise plain, sparse, workspace, delta, restore, branch merge, pull
merge and history replacement uploads, their failure paths, and the
project update fields and refusals. A fake Rotur answers profile lookups
so team changes and transfers run offline, and the run ends with each
stored project record and a blob inventory.
handleUploadProjectLocked and handleUpdateProjectLocked now read as a
sequence of small steps that each return a Refusal record, so every
early return keeps its status, body and order. Upload state moves
through UploadRequest, UploadHistory, UploadArchive and UploadRollback
records; update state through OwnerTransfer. Pure decisions live in
policy files with tests, and the request, refusal and editor scope
helpers are shared by both handlers.
refused() turned any status other than 400, 403 and 404 into a 500.
Give 500 its own branch and send any other status as it is, with an
ok: false body. No current refusal uses another status, so responses
are unchanged.
Every source file lists its exports after its imports; project-update.osl
had none. Its OwnerTransfer record moves to project-update-policy.osl,
next to how the upload step records sit in project-upload-policy.osl.
project-upload-policy.osl was collecting helpers that are not upload
policy. keysNotIn, storedWorkspaceLayers, replacedWorkspaceKeys and
projectFilesErrorBody move to project-upload-publish.osl, and megabytes
with the three size failure helpers move to project-upload-files.osl.
Their tests move into new test files beside those sources.
The split carried three comments over unchanged. New code here has no
comments, and the function and parameter names already say what they
described.
The smoke test masked far more than was volatile. Its key pattern was
case-insensitive, so suffix rules also hid dataFormat, allowedItems,
item and product ids, credit URLs, jsonKey and workspaceKey, and sizes
were rounded to KiB, which would hide a character count used as a byte
count.

Mask only named volatile keys (tokens, view keys, timestamps, history
sizes, notification ids and the history-sized quota figures), random
access keys in URLs and embedded hashes and times. Sizes are compared
exactly, and staged chunk sizes are checked against the bytes sent.
Every successful upload and update now records the stored project, and
the blob inventory lists the exact asset and project JSON names. A
non-ASCII project and description tell characters from bytes.
Add the refusals that can be reached offline: a corrupt sb3 that only
fails to decompress, an oversized asset (project_too_large), a full
Rotur storage (storage_full, from a fake storage ledger that refuses one
user), a missing worktree asset, history that does not descend from the
saved head, a merge tree that cannot be merged, and a pull whose source
remix was deleted.

Three more steps make a data directory read-only for one request to
reach the failure paths after the files are accepted: storing extension
sources, storing the project JSON and saving the project record. They
answer 500 by design, so they are listed as expected errors.

checkGeneratedHistory cannot be reached offline: merge and restore
archives are built to match the stored history they are checked
against.
@Mistium
Mistium merged commit 165dca0 into main Nov 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant